package v2

import (
	"context"
	"fmt"
	"strconv"
	"time"

	"github.com/shopspring/decimal"

	"github.com/smartcontractkit/chainlink-common/pkg/capabilities"
	"github.com/smartcontractkit/chainlink-common/pkg/values"
	sdkpb "github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk/v2/pb"
	"github.com/smartcontractkit/chainlink-common/pkg/workflows/wasm/host"
	protoevents "github.com/smartcontractkit/chainlink-protos/workflows/go/events"
	"github.com/smartcontractkit/chainlink/v2/core/platform"
	"github.com/smartcontractkit/chainlink/v2/core/services/workflows/events"
	"github.com/smartcontractkit/chainlink/v2/core/services/workflows/store"
)

var _ host.ExecutionHelper = (*ExecutionHelper)(nil)

type ExecutionHelper struct {
	*Engine
	WorkflowExecutionID string
	UserLogChan         chan<- *protoevents.LogLine
	TimeProvider
	SecretsFetcher
}

// CallCapability handles requests generated by the wasm guest
func (c *ExecutionHelper) CallCapability(ctx context.Context, request *sdkpb.CapabilityRequest) (*sdkpb.CapabilityResponse, error) {
	return c.capCallsSemaphore.WhenAcquired(ctx, func() (*sdkpb.CapabilityResponse, error) {
		return c.callCapability(ctx, request)
	})
}

func (c *ExecutionHelper) callCapability(ctx context.Context, request *sdkpb.CapabilityRequest) (*sdkpb.CapabilityResponse, error) {
	// TODO (CAPPL-735): use request.Metadata.WorkflowExecutionId to associate the call with a specific execution
	capability, err := c.cfg.CapRegistry.GetExecutable(ctx, request.Id)
	if err != nil {
		return nil, fmt.Errorf("trigger capability not found: %w", err)
	}

	info, err := capability.Info(ctx)
	if err != nil {
		return nil, fmt.Errorf("capability info not found: %w", err)
	}

	// If the capability info is missing a DON, then
	// the capability is local, and we should use the localNode's DON ID.
	var donID uint32
	if !info.IsLocal {
		donID = info.DON.ID
	} else {
		donID = c.localNode.WorkflowDON.ID
	}

	config, err := c.cfg.CapRegistry.ConfigForCapability(ctx, info.ID, donID)
	if err != nil {
		// not explicitly an error case and more relevant (helpful) logging occurs in the metering package
		// debug level should be sufficient here
		c.lggr.Debugf("capability config not found: %s", err)
	}

	capReq := capabilities.CapabilityRequest{
		Payload:      request.Payload,
		Method:       request.Method,
		CapabilityId: request.Id,
		Metadata: capabilities.RequestMetadata{
			WorkflowExecutionID: c.WorkflowExecutionID,
			ReferenceID:         strconv.Itoa(int(request.CallbackId)),
		},
		Config: values.EmptyMap(),
	}

	meterReport, ok := c.meterReports.Get(c.WorkflowExecutionID)
	if !ok {
		c.lggr.Errorf("no metering report found for %v", c.WorkflowExecutionID)
	}
	meteringRef := strconv.Itoa(int(request.CallbackId))

	// TODO: https://smartcontract-it.atlassian.net/browse/CRE-285 get max spend per step from SDK.
	// TODO: https://smartcontract-it.atlassian.net/browse/CRE-284 parse user max spend for step
	userSpendLimit := decimal.NewNullDecimal(decimal.Zero)
	userSpendLimit.Valid = false

	spendLimit, err := meterReport.GetMaxSpendForInvocation(userSpendLimit, int(c.cfg.LocalLimits.MaxConcurrentCapabilityCallsPerWorkflow)-c.capCallsSemaphore.Len())
	if err != nil {
		c.lggr.Errorw("could not reserve for capability request", "capReq", request.Id, "capReqCallbackID", request.CallbackId, "err", err)
	}

	if spendLimit.Valid {
		if err = meterReport.Deduct(meteringRef, spendLimit.Decimal); err != nil {
			c.cfg.Lggr.Errorw("could not deduct balance for capability request", "capReq", request.Id, "capReqCallbackID", request.CallbackId, "err", err)
		}

		// the nil case for config.RestrictedConfig is and should be handled by CreditToSpendingLimits
		capReq.Metadata.SpendLimits = meterReport.CreditToSpendingLimits(info, config.RestrictedConfig, spendLimit.Decimal)
	}

	c.lggr.Debugw("Executing capability ...", "capID", request.Id, "capReqCallbackID", request.CallbackId, "capReqMethod", request.Method)
	c.metrics.With(platform.KeyCapabilityID, request.Id).IncrementCapabilityInvocationCounter(ctx)
	_ = events.EmitCapabilityStartedEvent(ctx, c.loggerLabels, c.WorkflowExecutionID, request.Id, meteringRef)

	execCtx, execCancel := context.WithTimeout(ctx, time.Millisecond*time.Duration(c.cfg.LocalLimits.CapabilityCallTimeoutMs))
	defer execCancel()

	capResp, err := capability.Execute(execCtx, capReq)
	if err != nil {
		c.lggr.Debugw("Capability execution failed", "capID", request.Id, "capReqCallbackID", request.CallbackId, "err", err)
		_ = events.EmitCapabilityFinishedEvent(ctx, c.loggerLabels, c.WorkflowExecutionID, request.Id, meteringRef, store.StatusErrored)
		c.metrics.With(platform.KeyCapabilityID, request.Id).IncrementCapabilityFailureCounter(ctx)
		c.metrics.IncrementTotalWorkflowStepErrorsCounter(ctx)
		return nil, fmt.Errorf("failed to execute capability: %w", err)
	}

	c.lggr.Debugw("Capability execution succeeded", "capID", request.Id, "capReqCallbackID", request.CallbackId)
	_ = events.EmitCapabilityFinishedEvent(ctx, c.loggerLabels, c.WorkflowExecutionID, request.Id, meteringRef, store.StatusCompleted)

	err = meterReport.Settle(meteringRef, capResp.Metadata.Metering)
	if err != nil {
		c.lggr.Errorw("failed to set metering for capability request", "capReq", request.Id, "capReqCallbackID", request.CallbackId, "err", err)
	}

	return &sdkpb.CapabilityResponse{
		Response: &sdkpb.CapabilityResponse_Payload{
			Payload: capResp.Payload,
		},
	}, nil
}

func (c *ExecutionHelper) GetWorkflowExecutionID() string {
	return c.WorkflowExecutionID
}

func (c *ExecutionHelper) EmitUserLog(msg string) error {
	select {
	case c.UserLogChan <- &protoevents.LogLine{
		NodeTimestamp: time.Now().Format(time.RFC3339Nano),
		Message:       msg,
	}:
		// Successfully sent to channel
	default:
		c.lggr.Warnw("Exceeded max allowed user log messages, dropping")
	}
	return nil
}
